#!/usr/bin/env bash
##
## Git-only helper to generate Conventional Commits messages and commit.
## 仅依赖 Git 与 Shell；不调用任何包管理器或构建工具。
##
## 功能概述：
## - 校验仓库/分支状态（冲突/重排除）
## - 检测已暂存与未暂存改动，必要时在 --all 下自动 `git add -A`
## - 拆分建议（按关注点/目录/文件类型），提供 pathspec 示例
## - 自动生成 Conventional Commits 提交信息（可选 --emoji、--scope、--type）
## - 写入 .git/COMMIT_EDITMSG 并执行 `git commit`（默认尊重钩子，除非 --no-verify）
## - 可使用 --amend、--signoff
##
## 注意：本脚本不会修改工作区文件，仅操作暂存区与提交信息文件。

set -euo pipefail

########################################
# 选项解析与帮助
########################################

NO_VERIFY=0
ADD_ALL=0
AMEND=0
SIGNOFF=0
USE_EMOJI=0
SCOPE=""
FORCE_TYPE=""

print_usage() {
  cat <<'USAGE'
Usage:
  ./git-commit [--no-verify] [--all] [--amend] [--signoff] [--emoji]
               [--scope <scope>] [--type <type>]

Options:
  --no-verify   跳过本地 Git 钩子
  --all         当暂存区为空时，自动 `git add -A`
  --amend       修补上一次提交（不新建提交）
  --signoff     在提交信息末尾附加 Signed-off-by
  --emoji       在消息头部加入类型对应的 emoji
  --scope S     指定提交作用域（如 ui、docs、api）
  --type T      指定提交类型（feat/fix/docs/refactor/test/chore/style/ci/perf/revert）
  -h, --help    显示帮助

说明：
  - 若检测到多个相互独立的变更组，会给出拆分建议与 pathspec 示例；
    默认仅输出建议并退出，避免一次性提交不相关改动；
    如需强制一次提交，请先手工 `git add` 对应路径后再运行本命令。
USAGE
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --no-verify) NO_VERIFY=1; shift;;
    --all) ADD_ALL=1; shift;;
    --amend) AMEND=1; shift;;
    --signoff) SIGNOFF=1; shift;;
    --emoji) USE_EMOJI=1; shift;;
    --scope) SCOPE=${2:-}; shift 2;;
    --type) FORCE_TYPE=${2:-}; shift 2;;
    -h|--help) print_usage; exit 0;;
    *) echo "未知参数: $1" >&2; print_usage; exit 2;;
  esac
done

########################################
# 基础校验
########################################

if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  echo "错误：当前目录不在 Git 仓库内" >&2
  exit 1
fi

# 合并/重排/冲突状态检测
if [[ -d .git/rebase-merge || -d .git/rebase-apply ]]; then
  echo "提示：检测到 rebase 过程，请先完成或中止 rebase 后再运行。" >&2
  exit 1
fi
if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
  echo "提示：检测到合并尚未完成（存在 MERGE_HEAD）。请解决冲突并完成合并。" >&2
  exit 1
fi

# HEAD 状态（detached 检测）
BRANCH_NAME=""
if ! BRANCH_NAME=$(git symbolic-ref -q --short HEAD 2>/dev/null); then
  echo "警告：当前为 detached HEAD。将继续，但请确认你的意图。" >&2
fi

########################################
# 工具函数
########################################

trim() { sed 's/^\s\+//;s/\s\+$//' ; }

# 判断字符串是否包含中文字符
has_chinese() {
  LC_ALL=C awk '{
    for(i=1;i<=length($0);i++){
      c=substr($0,i,1);
      if (c ~ /[\x80-\xFF]/) { print 1; exit }
    }
    print 0
  }' <<<"$1" | tail -n1 | grep -q '^1$'
}

# 判定仓库提交语言倾向（根据最近 50 条 subject 是否含中文）
detect_lang() {
  local cnt_cn=0 cnt_total=0
  while IFS= read -r line; do
    ((cnt_total++)) || true
    if has_chinese "$line"; then ((cnt_cn++)) || true; fi
  done < <(git log -n 50 --pretty=%s 2>/dev/null || true)

  if (( cnt_total == 0 )); then
    echo "zh"
  else
    if (( cnt_cn * 2 >= cnt_total )); then echo "zh"; else echo "en"; fi
  fi
}

LANG_HINT=$(detect_lang)

echo "[info] repo: $(basename "$(git rev-parse --show-toplevel)")${BRANCH_NAME:+ branch: $BRANCH_NAME} lang: $LANG_HINT" >&2

# 获取状态（porcelain v1）
readarray -t STATUS_LINES < <(git status --porcelain 2>/dev/null)

get_index_status() {
  local line
  while IFS= read -r line; do
    local x y rest
    x="${line:0:1}"; y="${line:1:1}"; rest="${line:3}"
    if [[ "$x" != " " ]]; then echo "$x $y $rest"; fi
  done < <(printf '%s\n' "${STATUS_LINES[@]}")
}

get_worktree_status() {
  local line
  while IFS= read -r line; do
    local x y rest
    x="${line:0:1}"; y="${line:1:1}"; rest="${line:3}"
    if [[ "$y" != " " ]]; then echo "$x $y $rest"; fi
  done < <(printf '%s\n' "${STATUS_LINES[@]}")
}

count_index_changes() { get_index_status | wc -l | tr -d ' '; }
count_worktree_changes() { get_worktree_status | wc -l | tr -d ' '; }

# 若暂存区为空且指定 --all，则添加所有变更
if [[ "$(count_index_changes)" -eq 0 && $ADD_ALL -eq 1 ]]; then
  echo "[info] 暂存区为空，执行: git add -A" >&2
  git add -A
  readarray -t STATUS_LINES < <(git status --porcelain 2>/dev/null)
fi

STAGED_COUNT=$(count_index_changes)
UNSTAGED_COUNT=$(count_worktree_changes)

if [[ "$STAGED_COUNT" -eq 0 ]]; then
  echo "[hint] 暂存区无改动。" >&2
  if [[ $UNSTAGED_COUNT -gt 0 ]]; then
    echo "建议：" >&2
    echo "  - 使用 'git add -A' 将所有改动纳入本次提交，或" >&2
    echo "  - 按关注点分组手动暂存，例如：git add <paths>" >&2
  fi
  exit 1
fi

# 仅针对已暂存的文件继续分析
readarray -t STAGED_FILES < <(get_index_status | awk '{print substr($0,4)}')

# 提取顶级目录（或直接文件名）
top_of() {
  awk -F'/' '{print $1}' <<<"$1"
}

file_category() {
  local f="$1"
  local base=$(basename "$f")
  local ext="${f##*.}"
  case "$f" in
    docs/*|doc/*|*.md|*.rst|README|README.*) echo docs;;
    test/*|tests/*|__tests__/*|*.spec.*|*.test.*) echo test;;
    .github/*|.gitlab-ci.yml|Jenkinsfile|ci/*) echo ci;;
    *.yml|*.yaml|*.toml|*.ini|*.conf|*.config.*|*.properties|Makefile|Dockerfile|*.dockerfile) echo config;;
    *)
      case "$ext" in
        c|cc|cpp|h|hpp|js|ts|jsx|tsx|java|kt|go|rs|py|rb|php|sh|css|scss|vue|svelte) echo code;;
        *) echo other;;
      esac
    ;;
  esac
}

declare -A GROUPS_BY_CAT
declare -A GROUPS_BY_TOP

for f in "${STAGED_FILES[@]}"; do
  catg=$(file_category "$f")
  top=$(top_of "$f")
  GROUPS_BY_CAT["$catg"]+=" $f"
  GROUPS_BY_TOP["$top"]+=" $f"
done

cat_groups=0; for k in "${!GROUPS_BY_CAT[@]}"; do ((cat_groups++)) || true; done
top_groups=0; for k in "${!GROUPS_BY_TOP[@]}"; do ((top_groups++)) || true; done

# 统计 diff 规模
DIFF_SHORTSTAT=$(git diff --cached --shortstat || true)
ADDED=$(sed -n 's/.*\([0-9]\+\) insertions\?.*/\1/p' <<<"$DIFF_SHORTSTAT" | head -n1)
DELETED=$(sed -n 's/.*\([0-9]\+\) deletions\?.*/\1/p' <<<"$DIFF_SHORTSTAT" | head -n1)
ADDED=${ADDED:-0}
DELETED=${DELETED:-0}
TOTAL_LINES=$(( ADDED + DELETED ))

should_suggest_split=0
if (( cat_groups > 1 )); then should_suggest_split=1; fi
if (( top_groups > 2 )); then should_suggest_split=1; fi
if (( TOTAL_LINES > 300 )); then should_suggest_split=1; fi

if (( should_suggest_split == 1 )); then
  echo "[建议] 检测到改动可能需要拆分为多次提交：" >&2
  echo "  - 类别组数: $cat_groups（docs/test/ci/config/code/other）" >&2
  echo "  - 顶级目录组数: $top_groups" >&2
  echo "  - 行数规模: $TOTAL_LINES 行（新增 $ADDED, 删除 $DELETED）" >&2
  echo >&2
  echo "按类别拆分示例（任选其一执行）：" >&2
  for k in "${!GROUPS_BY_CAT[@]}"; do
    files=${GROUPS_BY_CAT[$k]}
    files=$(echo "$files" | trim)
    echo "  # $k" >&2
    echo "  git restore --staged :/ && git add $files && ./git-commit" >&2
  done
  echo >&2
  echo "按顶级目录拆分示例：" >&2
  for k in "${!GROUPS_BY_TOP[@]}"; do
    files=${GROUPS_BY_TOP[$k]}
    files=$(echo "$files" | trim)
    echo "  # $k/" >&2
    echo "  git restore --staged :/ && git add $files && ./git-commit" >&2
  done
  echo >&2
  echo "提示：为确保提交原子性，建议按上述分组分别提交。" >&2
  exit 2
fi

########################################
# 提交信息生成
########################################

emoji_for_type() {
  local t="$1"
  case "$t" in
    feat) echo "✨";;
    fix) echo "🐛";;
    docs) echo "📝";;
    style) echo "🎨";;
    refactor) echo "♻️";;
    perf) echo "⚡️";;
    test) echo "✅";;
    chore) echo "🔧";;
    ci) echo "👷";;
    revert) echo "⏪️";;
    *) echo "";;
  esac
}

infer_type_from_files() {
  local has_docs=0 has_test=0 has_ci=0 has_cfg=0 has_code=0 has_other=0
  for f in "${STAGED_FILES[@]}"; do
    case "$(file_category "$f")" in
      docs) has_docs=1;;
      test) has_test=1;;
      ci) has_ci=1;;
      config) has_cfg=1;;
      code) has_code=1;;
      *) has_other=1;;
    esac
  done
  local kinds=$(( has_docs + has_test + has_ci + has_cfg + has_code + has_other ))
  if (( kinds > 1 )); then
    echo "chore"; return
  fi
  if (( has_docs )); then echo "docs"; return; fi
  if (( has_test )); then echo "test"; return; fi
  if (( has_ci )); then echo "ci"; return; fi
  if (( has_cfg )); then echo "chore"; return; fi
  if (( has_code )); then echo "feat"; return; fi
  echo "chore"
}

infer_scope_from_files() {
  local scope=""
  if (( top_groups == 1 )); then
    for k in "${!GROUPS_BY_TOP[@]}"; do scope="$k"; done
    if [[ "$scope" == *.* || "$scope" == README* ]]; then
      scope=""
    fi
  fi
  echo "$scope"
}

count_added_modified_deleted() {
  local added=0 modified=0 deleted=0 renamed=0
  while IFS= read -r line; do
    local x y path
    x="${line:0:1}"; y="${line:1:1}"; path="${line:3}"
    case "$x" in
      A) ((added++))||true;;
      M) ((modified++))||true;;
      D) ((deleted++))||true;;
      R) ((renamed++))||true;;
    esac
  done < <(git diff --cached --name-status)
  echo "$added $modified $deleted $renamed"
}

gen_subject_core() {
  local a m d r
  read -r a m d r < <(count_added_modified_deleted)
  local action=""
  if (( a>0 && m==0 && d==0 )); then action="add"; fi
  if (( m>0 && a==0 && d==0 )); then action="update"; fi
  if (( d>0 && a==0 && m==0 )); then action="remove"; fi
  if [[ -z "$action" ]]; then action="adjust"; fi

  local noun="changes"
  local t="$1"
  case "$t" in
    docs) noun="docs";;
    test) noun="tests";;
    ci) noun="ci configs";;
    chore) noun="configs";;
    feat) noun="features";;
    refactor) noun="code structure";;
  esac

  if [[ "$LANG_HINT" == "zh" ]]; then
    case "$action" in
      add) echo "新增${noun}";;
      update) echo "更新${noun}";;
      remove) echo "移除${noun}";;
      *) echo "调整${noun}";;
    esac
  else
    echo "$action $noun"
  fi
}

truncate72() {
  local s="$1"
  local n=72
  printf "%s" "$s" | head -c $n | sed 's/[[:space:]]*$//'
}

TYPE=${FORCE_TYPE:-$(infer_type_from_files)}
if [[ -n "$FORCE_TYPE" ]]; then TYPE="$FORCE_TYPE"; fi

SCOPE_AUTO=$(infer_scope_from_files)
if [[ -z "$SCOPE" ]]; then SCOPE="$SCOPE_AUTO"; fi

SUBJECT_CORE=$(gen_subject_core "$TYPE")

HEADER=""
if (( USE_EMOJI == 1 )); then
  EMOJI=$(emoji_for_type "$TYPE")
  if [[ -n "$EMOJI" ]]; then HEADER+="$EMOJI "; fi
fi
HEADER+="$TYPE"
if [[ -n "$SCOPE" ]]; then HEADER+="($SCOPE)"; fi
HEADER+=": $SUBJECT_CORE"
HEADER=$(truncate72 "$HEADER")

bullet() {
  if [[ "$LANG_HINT" == "zh" ]]; then
    echo "- $1"
  else
    echo "- $1"
  fi
}

BODY=""
if [[ "$LANG_HINT" == "zh" ]]; then
  BODY+=$(bullet "动机：基于已暂存改动生成规范化提交信息")$'\n'
  BODY+=$(bullet "实现要点：自动推断类型/作用域，限制标题 72 字符")$'\n'
  BODY+=$(bullet "影响范围：$(printf '%s\n' "${!GROUPS_BY_TOP[@]}" | tr '\n' ' ' | sed 's/ *$//')")$'\n'
  BODY+=$(bullet "改动规模：新增 $ADDED 行，删除 $DELETED 行")$'\n'
  BODY+=$(bullet "建议：保持提交原子性，如有需要请拆分提交")$'\n'
else
  BODY+=$(bullet "Motivation: generate conventional commit from staged changes")$'\n'
  BODY+=$(bullet "Highlights: infer type/scope, 72-char subject limit")$'\n'
  BODY+=$(bullet "Touched: $(printf '%s\n' "${!GROUPS_BY_TOP[@]}" | tr '\n' ' ' | sed 's/ *$//')")$'\n'
  BODY+=$(bullet "Diff size: +$ADDED -$DELETED lines")$'\n'
  BODY+=$(bullet "Advice: keep commits atomic; split when necessary")$'\n'
fi

if (( SIGNOFF == 1 )); then
  SIGN_OPT="-s"
else
  SIGN_OPT=""
fi

if (( NO_VERIFY == 1 )); then
  NV_OPT="--no-verify"
else
  NV_OPT=""
fi

if (( AMEND == 1 )); then
  AMEND_OPT="--amend"
else
  AMEND_OPT=""
fi

COMMIT_FILE=".git/COMMIT_EDITMSG"
{
  printf '%s\n\n' "$HEADER"
  printf '%s\n' "$BODY"
} > "$COMMIT_FILE"

echo "[info] 提交消息已写入 $COMMIT_FILE" >&2
echo "--------8<-------- COMMIT_EDITMSG --------8<--------" >&2
sed -n '1,80p' "$COMMIT_FILE" >&2
echo "--------8<----------------------------------------" >&2

set +e
git commit $NV_OPT $SIGN_OPT $AMEND_OPT -F "$COMMIT_FILE"
rc=$?
set -e

if [[ $rc -ne 0 ]]; then
  echo "[error] git commit 失败（退出码 $rc）。请检查钩子或参数。" >&2
  exit $rc
fi

echo "[done] 提交完成。"
